<!DOCTYPE html>
<html lang="en">

<head>
	<meta charset="utf-8"> <!-- 342-294 -->
	<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
	
	<link rel="stylesheet" href="styles.css">

	<script type="importmap">
	  {
		"imports": {
			"three": "https://cdn.jsdelivr.net/npm/three@0.164.0/build/three.module.js",
			"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.164.0/examples/jsm/",
			"three/nodes": "https://cdn.jsdelivr.net/npm/three@0.164.0/examples/jsm/nodes/Nodes.js"
		}
	  }
	</script>

</head>


<body>
	<div class="header white">
		<h1 class="white">Equirectangular Texture Generator</h1>
		<h3 class="white">Proof of concept: using Three.js Shading Language for dynamic textures</h3>
	</div>
	
	<div class="footnote white">
		<a href="#" onclick="window.history.back(); return false;">Back</a>
	</div>	
	

	<script type="module">

		import * as THREE from 'three';
		import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
		import { TeapotGeometry } from 'three/addons/geometries/TeapotGeometry.js';
		import WebGPURenderer from 'three/addons/renderers/webgpu/WebGPURenderer.js';
		import { MeshPhysicalNodeMaterial, positionLocal, timerLocal, tslFn, vec3, If, cond, sub, sin, mul } from 'three/nodes';
		import { mx_perlin_noise_float } from 'three/addons/nodes/materialx/lib/mx_noise.js';



		// general setup of renderer, scene, camera and light
		
		var renderer = new WebGPURenderer( { antialias: true } );
		renderer.setPixelRatio( devicePixelRatio );
		renderer.setSize( innerWidth, innerHeight );
		renderer.shadowMap.enabled = true;
		renderer.shadowMap.type = THREE.PCFSoftShadowMap;
		document.body.appendChild( renderer.domElement );

		var scene = new THREE.Scene();
		scene.background = new THREE.Color( 'black' );

		var camera = new THREE.PerspectiveCamera( 27, innerWidth / innerHeight, 1, 1000 );
		camera.position.set( -30, 10, 40 );

		var light = new THREE.DirectionalLight( 'white', 1 );
		light.position.set( 0, 0, 100 );
		light.castShadow = true;
		light.shadow.bias = -0.01;
		light.shadow.mapSize.width = 512;
		light.shadow.mapSize.height = 512;
		light.shadow.camera.left = -10;
		light.shadow.camera.right = 10;
		light.shadow.camera.top = 4;
		light.shadow.camera.bottom = -4;
		light.shadow.camera.near = 75;
		light.shadow.camera.far = 125;
		light.shadow.autoUpdate = false;
		
		scene.add( light );

		scene.add( new THREE.AmbientLight( 'white', 0.05 ) );



		// stars, planet and satellite
		
		var stars = new THREE.Points(
			new THREE.IcosahedronGeometry( 1, 10 ),
			new THREE.PointsMaterial( { color: 'white' } )
		);
		
		var starsPos = stars.geometry.getAttribute( 'position' ),
			v = new THREE.Vector3();
		
		for ( var i=0; i<starsPos.count; i++ ) {

			v.randomDirection().setLength( 100 );
			starsPos.setXYZ( i, v.x, v.y, v.z );
		
		}
		
		var ball = new THREE.IcosahedronGeometry( 8, 10 ),
			teapot = new TeapotGeometry( 2 ),
			material = new MeshPhysicalNodeMaterial( { roughness: 0.5, metalness: 0.5 } );
		
		var planet = new THREE.Mesh( ball, material );
		planet.castShadow = true;
		planet.receiveShadow = true;

		
		var satellite = new THREE.Mesh( teapot, material );
		satellite.castShadow = true;
		satellite.receiveShadow = true;

		scene.add( stars, planet, satellite );
		

		
		// user navigation and resize events
		
		new OrbitControls( camera, renderer.domElement );

		window.addEventListener( 'resize', onWindowResize );

		function onWindowResize() {

			camera.aspect = innerWidth / innerHeight;
			camera.updateProjectionMatrix();

			renderer.setSize( innerWidth, innerHeight );

		}



		// the animation loop
		
		renderer.setAnimationLoop( ( time ) => {

			if ( satellite ) {

				satellite.position.setFromSphericalCoords( 15, Math.PI/2, time/1000 );
				satellite.rotation.set( time/600, -time/700, time/850 );
		
	}

			renderer.render( scene, camera );

		} );


		
		// all TSL stuff comes here



		// helper function - convert hue to rgb

		var hue2rgb = tslFn( ([ p, t, q ]) => {

			t = t.add( cond( t.lessThan( 0 ), 1, 0 ) );
			t = t.sub( cond( t.greaterThan( 1 ), 1, 0 ) );

			If( t.lessThan( 1/6 ), ( ) => {

				return mul( q.sub( p ), t, 6 ).add( p );

			} );
			If( t.lessThan( 1/2 ), () => {

				return q;

			} );
			If( t.lessThan( 2/3 ), () => {

				return mul( q.sub( p ), sub( 2/3, t ), 6 ).add( p );

			} );

			return p;

		} );

		hue2rgb.setLayout( {
			name: 'hue2rgb',
			type: 'float',
			inputs: [
				{ name: 'p', type: 'float' },
				{ name: 't', type: 'float' },
				{ name: 'g', type: 'float' },
			]
		} );


		// helper function - convert hsl to rgb

		const setHSL = tslFn( ([ h, s, l ]) => {

			h = ( h.fract() );
			s = ( s.clamp( 0, 1 ) );
			l = ( l.clamp( 0, 1 ) );
		
			If( s.equal( 0 ), ( ) => {

				return vec3( l, l, l );

			} );

			var p = cond( l.lessThanEqual( 0.5 ), l.mul( s.add( 1 ) ), l.add( s ).sub( l.mul( s ) ) );
			var q = l.mul( 2 ).sub( p );

			var r = hue2rgb( q, p, h.add( 1/3 ) );
			var g = hue2rgb( q, p, h );
			var b = hue2rgb( q, p, h.sub( 1/3 ) );
		
			return vec3( r, g, b );
		
		} );

		setHSL.setLayout( {
			name: 'setHSL',
			type: 'vec3',
			inputs: [
				{ name: 'h', type: 'float' },
				{ name: 's', type: 'float' },
				{ name: 'v', type: 'float' },
			]
		} );



		// dynamic texture
		
		var dynaTexture = tslFn( ([ pos ]) => {
		
			pos = pos.mul( 0.25 );
		
			var k = mx_perlin_noise_float(
				vec3(
					mx_perlin_noise_float( pos ),
					mx_perlin_noise_float( pos ).mul( 2 ),
					mx_perlin_noise_float( pos ).mul( 3 ),
				)
			);

			return setHSL(
				k.mul( 1.5 ).add( 0.3 ),
				1,
				sin( mul( k, Math.PI, 2 ) ).mul( 0.5 ).add( 0.5 )
			).mul( 5 );
		
		} );



		// bind the dynamic texture with the planet/satellite material
		
		var posNode = positionLocal.mul( 2 ).add( timerLocal( 2 ) );

		material.colorNode = dynaTexture( posNode );

	</script>
</body>
</html>